package com.appistry.api.rest.impl;

import java.io.IOException;
import java.io.InputStream;

import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpConnectionManager;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.params.HostParams;
import org.apache.commons.httpclient.params.HttpClientParams;
import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
import org.apache.commons.httpclient.params.HttpMethodParams;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.appistry.api.rest.RestConnection;
import com.appistry.api.rest.domain.RestResponse;
import com.appistry.api.rest.exception.RestException;
import com.appistry.api.rest.httpcommons.InputStreamRequestEntity;

public abstract class AbstractRestConnection implements RestConnection {
	private static final Log LOG = LogFactory.getLog(AbstractRestConnection.class);
	private final HttpMethodParams httpMethodParams = new HttpMethodParams();
	private final HttpConnectionManagerParams httpConnectionManagerParams = new HttpConnectionManagerParams();
	private final HostParams hostParams = new HostParams();
	private final HttpClientParams httpClientParams = new HttpClientParams();

	private static HttpConnectionManager httpConnectionManager;
	private HttpClient httpClient;

	public AbstractRestConnection(){
		//Set some default params
		
		// Default UTF-8
		httpClientParams.setContentCharset("UTF-8");
	}
	
	public RestResponse delete(String path) throws RestException {
		try {
			DeleteMethod method = new DeleteMethod(path);
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		}
	}

	public RestResponse get(String path) throws RestException {
		try {
			GetMethod method = new GetMethod(path);
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		}
	}

	public RestResponse head(String path) throws RestException {
		HeadMethod headMethod = null;

		try {
			headMethod = new HeadMethod(path);
			return execute(headMethod);
		} catch (IOException e) {
			throw new RestException(e);
		} finally {
			if (headMethod != null) {
				headMethod.releaseConnection();
			}
		}
	}

	public RestResponse move(String path, String dest) throws RestException {
		try {
			PostMethod method = new PostMethod(path);
			method.setRequestHeader("X-Method-Override", "MOVE");
			method.setRequestHeader("destination", dest);
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		}
	}

	public RestResponse post(String path, InputStream content) throws RestException {
		try {
			PostMethod method = new PostMethod(path);
			method.setRequestEntity(new InputStreamRequestEntity(content));
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		} finally {
			close(content);
		}
	}

	public RestResponse put(String path, InputStream content) throws RestException {
		try {
			PutMethod method = new PutMethod(path);
			method.setRequestEntity(new InputStreamRequestEntity(content));
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		} finally {
			close(content);
		}
	}

	public RestResponse put(String path, long length, InputStream content) throws RestException {
		try {
			PutMethod method = new PutMethod(path);
			method.setRequestEntity(new InputStreamRequestEntity(content, length));
			RestResponse response = execute(method);

			return response;
		} catch (IOException e) {
			throw new RestException(e);
		} finally {
			close(content);
		}
	}

	/**
	 * Helper method to close input stream
	 * 
	 * @param is
	 */
	protected void close(InputStream is) {
		if (is != null) {
			try {
				is.close();
			} catch (IOException e) {
				LOG.warn("Unable to close InputStream!", e);
			}
		}
	}

	/**
	 * Executes the http method and returns a RestResponse
	 * 
	 * @param method
	 * @return
	 * @throws IOException
	 */
	private RestResponse execute(HttpMethod method) throws IOException {
		// Set method params
		// method.setParams(httpMethodParams);

		// Get the http(s) client
		HttpClient client = getHttpClient();

		// Execute
		client.executeMethod(method);

		// Return the RestResponse
		return new RestResponse(method);
	}

	/**
	 * Singleton pattern to initialize connection manager. 
	 * @return
	 */
	private synchronized HttpConnectionManager getHttpConnectionManager() {
		if (httpConnectionManager == null) {
			// Create HttpConnectionManager that releases connections
			httpConnectionManager = new MultiThreadedHttpConnectionManager();

			// Set params
			httpConnectionManager.setParams(httpConnectionManagerParams);			
		}

		return httpConnectionManager;
	}

	/**
	 * Creates an instance of HttpClient if one doesn't exist already.
	 * @return
	 */
	private synchronized HttpClient getHttpClient() {
		if(httpClient == null){
			// Create HttpClient
			httpClient = new HttpClient(getHttpConnectionManager());
	
			// Set params
			httpClient.setParams(httpClientParams);
	
			// Set host using HTTP(s) protocol
			httpClient.setHostConfiguration(getHostConfiguration());
	
			// Add any credentials
			UsernamePasswordCredentials credentials = getUsernamePasswordCredentials();
			if (credentials != null) {
				// Create AuthScope
				AuthScope authScope = new AuthScope(getHostConfiguration().getHost(), getHostConfiguration().getPort());
	
				// Set the creds
				httpClient.getState().setCredentials(authScope, credentials);
			}
			
		}

		
		return httpClient;
	}

	// Abstracted out for HTTP vs HTTPS
	protected abstract HostConfiguration getHostConfiguration();

	protected abstract UsernamePasswordCredentials getUsernamePasswordCredentials();

	/*** http parameters ***/
	public HttpMethodParams getHttpMethodParams() {
		return httpMethodParams;
	}

	public HttpConnectionManagerParams getHttpConnectionManagerParams() {
		return httpConnectionManagerParams;
	}

	public HostParams getHostParams() {
		return hostParams;
	}

	public HttpClientParams getHttpClientParams() {
		return httpClientParams;
	}

}
